( function () {
/**
 * Description: reads BVH files and outputs a single THREE.Skeleton and an THREE.AnimationClip
 *
 * Currently only supports bvh files containing a single root.
 *
 */

class BVHLoader extends THREE.Loader {
  constructor(manager) {
    super(manager);
    this.animateBonePositions = true;
    this.animateBoneRotations = true;
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this;
    const loader = new THREE.FileLoader(scope.manager);
    loader.setPath(scope.path);
    loader.setRequestHeader(scope.requestHeader);
    loader.setWithCredentials(scope.withCredentials);
    loader.load(url, function (text) {
      try {
        onLoad(scope.parse(text));
      } catch (e) {
        if (onError) {
          onError(e);
        } else {
          console.error(e);
        }

        scope.manager.itemError(url);
      }
    }, onProgress, onError);
  }

  parse(text) {
    /*
    	reads a string array (lines) from a BVH file
    	and outputs a skeleton structure including motion data
    		returns thee root node:
    	{ name: '', channels: [], children: [] }
    */
    function readBvh(lines) {
      // read model structure
      if (nextLine(lines) !== 'HIERARCHY') {
        console.error('THREE.BVHLoader: HIERARCHY expected.');
      }

      const list = []; // collects flat array of all bones

      const root = readNode(lines, nextLine(lines), list); // read motion data

      if (nextLine(lines) !== 'MOTION') {
        console.error('THREE.BVHLoader: MOTION expected.');
      } // number of frames


      let tokens = nextLine(lines).split(/[\s]+/);
      const numFrames = parseInt(tokens[1]);

      if (isNaN(numFrames)) {
        console.error('THREE.BVHLoader: Failed to read number of frames.');
      } // frame time


      tokens = nextLine(lines).split(/[\s]+/);
      const frameTime = parseFloat(tokens[2]);

      if (isNaN(frameTime)) {
        console.error('THREE.BVHLoader: Failed to read frame time.');
      } // read frame data line by line


      for (let i = 0; i < numFrames; i++) {
        tokens = nextLine(lines).split(/[\s]+/);
        readFrameData(tokens, i * frameTime, root);
      }

      return list;
    }
    /*
    	Recursively reads data from a single frame into the bone hierarchy.
    	The passed bone hierarchy has to be structured in the same order as the BVH file.
    	keyframe data is stored in bone.frames.
    		- data: splitted string array (frame values), values are shift()ed so
    	this should be empty after parsing the whole hierarchy.
    	- frameTime: playback time for this keyframe.
    	- bone: the bone to read frame data from.
    */


    function readFrameData(data, frameTime, bone) {
      // end sites have no motion data
      if (bone.type === 'ENDSITE') return; // add keyframe

      const keyframe = {
        time: frameTime,
        position: new THREE.Vector3(),
        rotation: new THREE.Quaternion()
      };
      bone.frames.push(keyframe);
      const quat = new THREE.Quaternion();
      const vx = new THREE.Vector3(1, 0, 0);
      const vy = new THREE.Vector3(0, 1, 0);
      const vz = new THREE.Vector3(0, 0, 1); // parse values for each channel in node

      for (let i = 0; i < bone.channels.length; i++) {
        switch (bone.channels[i]) {
          case 'Xposition':
            keyframe.position.x = parseFloat(data.shift().trim());
            break;

          case 'Yposition':
            keyframe.position.y = parseFloat(data.shift().trim());
            break;

          case 'Zposition':
            keyframe.position.z = parseFloat(data.shift().trim());
            break;

          case 'Xrotation':
            quat.setFromAxisAngle(vx, parseFloat(data.shift().trim()) * Math.PI / 180);
            keyframe.rotation.multiply(quat);
            break;

          case 'Yrotation':
            quat.setFromAxisAngle(vy, parseFloat(data.shift().trim()) * Math.PI / 180);
            keyframe.rotation.multiply(quat);
            break;

          case 'Zrotation':
            quat.setFromAxisAngle(vz, parseFloat(data.shift().trim()) * Math.PI / 180);
            keyframe.rotation.multiply(quat);
            break;

          default:
            console.warn('THREE.BVHLoader: Invalid channel type.');
        }
      } // parse child nodes


      for (let i = 0; i < bone.children.length; i++) {
        readFrameData(data, frameTime, bone.children[i]);
      }
    }
    /*
     Recursively parses the HIERACHY section of the BVH file
    	 - lines: all lines of the file. lines are consumed as we go along.
     - firstline: line containing the node type and name e.g. 'JOINT hip'
     - list: collects a flat list of nodes
    	 returns: a BVH node including children
    */


    function readNode(lines, firstline, list) {
      const node = {
        name: '',
        type: '',
        frames: []
      };
      list.push(node); // parse node type and name

      let tokens = firstline.split(/[\s]+/);

      if (tokens[0].toUpperCase() === 'END' && tokens[1].toUpperCase() === 'SITE') {
        node.type = 'ENDSITE';
        node.name = 'ENDSITE'; // bvh end sites have no name
      } else {
        node.name = tokens[1];
        node.type = tokens[0].toUpperCase();
      }

      if (nextLine(lines) !== '{') {
        console.error('THREE.BVHLoader: Expected opening { after type & name');
      } // parse OFFSET


      tokens = nextLine(lines).split(/[\s]+/);

      if (tokens[0] !== 'OFFSET') {
        console.error('THREE.BVHLoader: Expected OFFSET but got: ' + tokens[0]);
      }

      if (tokens.length !== 4) {
        console.error('THREE.BVHLoader: Invalid number of values for OFFSET.');
      }

      const offset = new THREE.Vector3(parseFloat(tokens[1]), parseFloat(tokens[2]), parseFloat(tokens[3]));

      if (isNaN(offset.x) || isNaN(offset.y) || isNaN(offset.z)) {
        console.error('THREE.BVHLoader: Invalid values of OFFSET.');
      }

      node.offset = offset; // parse CHANNELS definitions

      if (node.type !== 'ENDSITE') {
        tokens = nextLine(lines).split(/[\s]+/);

        if (tokens[0] !== 'CHANNELS') {
          console.error('THREE.BVHLoader: Expected CHANNELS definition.');
        }

        const numChannels = parseInt(tokens[1]);
        node.channels = tokens.splice(2, numChannels);
        node.children = [];
      } // read children


      while (true) {
        const line = nextLine(lines);

        if (line === '}') {
          return node;
        } else {
          node.children.push(readNode(lines, line, list));
        }
      }
    }
    /*
    	recursively converts the internal bvh node structure to a THREE.Bone hierarchy
    		source: the bvh root node
    	list: pass an empty array, collects a flat list of all converted THREE.Bones
    		returns the root THREE.Bone
    */


    function toTHREEBone(source, list) {
      const bone = new THREE.Bone();
      list.push(bone);
      bone.position.add(source.offset);
      bone.name = source.name;

      if (source.type !== 'ENDSITE') {
        for (let i = 0; i < source.children.length; i++) {
          bone.add(toTHREEBone(source.children[i], list));
        }
      }

      return bone;
    }
    /*
    	builds a THREE.AnimationClip from the keyframe data saved in each bone.
    		bone: bvh root node
    		returns: a THREE.AnimationClip containing position and quaternion tracks
    */


    function toTHREEAnimation(bones) {
      const tracks = []; // create a position and quaternion animation track for each node

      for (let i = 0; i < bones.length; i++) {
        const bone = bones[i];
        if (bone.type === 'ENDSITE') continue; // track data

        const times = [];
        const positions = [];
        const rotations = [];

        for (let j = 0; j < bone.frames.length; j++) {
          const frame = bone.frames[j];
          times.push(frame.time); // the animation system animates the position property,
          // so we have to add the joint offset to all values

          positions.push(frame.position.x + bone.offset.x);
          positions.push(frame.position.y + bone.offset.y);
          positions.push(frame.position.z + bone.offset.z);
          rotations.push(frame.rotation.x);
          rotations.push(frame.rotation.y);
          rotations.push(frame.rotation.z);
          rotations.push(frame.rotation.w);
        }

        if (scope.animateBonePositions) {
          tracks.push(new THREE.VectorKeyframeTrack('.bones[' + bone.name + '].position', times, positions));
        }

        if (scope.animateBoneRotations) {
          tracks.push(new THREE.QuaternionKeyframeTrack('.bones[' + bone.name + '].quaternion', times, rotations));
        }
      }

      return new THREE.AnimationClip('animation', -1, tracks);
    }
    /*
    	returns the next non-empty line in lines
    */


    function nextLine(lines) {
      let line; // skip empty lines

      while ((line = lines.shift().trim()).length === 0) {}

      return line;
    }

    const scope = this;
    const lines = text.split(/[\r\n]+/g);
    const bones = readBvh(lines);
    const threeBones = [];
    toTHREEBone(bones[0], threeBones);
    const threeClip = toTHREEAnimation(bones);
    return {
      skeleton: new THREE.Skeleton(threeBones),
      clip: threeClip
    };
  }

}

THREE.BVHLoader = BVHLoader;
} )();
